﻿#region --- License & Copyright Notice ---
/*
ConsoleFx CommandLine Processing Library

Copyright (c) 2006-2012 Jeevan James
All rights reserved.

The contents of this file are made available under the terms of the
Eclipse Public License v1.0 (the "License") which accompanies this
distribution, and is available at the following URL:
http://opensource.org/licenses/eclipse-1.0.txt

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

By using this software in any fashion, you are agreeing to be bound by the
terms of the License.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;

using ConsoleFx.Parsers;
using ConsoleFx.Resources;
using ConsoleFx.Validators;

namespace ConsoleFx.Programs
{
    internal static class FluentArgumentHelpers
    {
        internal static void ValidateWith<T>(T argument, params BaseValidator[] validators) where T : Argument
        {
            if (validators == null)
                throw new ArgumentNullException("validators");
            if (validators.Length == 0)
                throw new ArgumentException(ConsoleProgramMessages.ArgumentValidatorsNotSpecified, "validators");
            foreach (BaseValidator validator in validators)
                argument.Validators.Add(validator);
        }

        internal static void HandledBy<T>(T argument, ArgumentHandler handler) where T : Argument
        {
            if (handler == null)
                throw new ArgumentNullException("handler");
            argument.Handler = handler;
        }

        internal static void AssignTo<T>(Argument argument, Expression<Func<T>> expression, Converter<string, T> converter = null)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            argument.Handler = arg => {
                converter = FluentHelpers.GetConverterFor(converter);
                MemberInfo dataMember = FluentHelpers.GetMemberInfoFromExpression(expression);
                object value = converter != null ? (object)converter(arg) : arg;
                FluentHelpers.SetDataMemberValue(dataMember, value);
            };
        }
    }

    internal static class FluentOptionHelpers
    {
        internal static void ValidateWith<T>(T option, params BaseValidator[] validators) where T : Option
        {
            if (validators == null)
                throw new ArgumentNullException("validators");
            if (validators.Length == 0)
                throw new ArgumentException(
                    ConsoleProgramMessages.OptionValidatorsNotSpecified.Fmt(CultureInfo.CurrentCulture, option.Name), "validators");
            foreach (BaseValidator validator in validators)
                option.Validators.Add(validator);
        }

        internal static void ValidateWith<T>(T option, int parameterIndex, params BaseValidator[] validators) where T : Option
        {
            if (parameterIndex < ParameterIndex.All)
                throw new ArgumentOutOfRangeException("parameterIndex", ConsoleProgramMessages.InvalidParameterIndex);
            if (validators == null)
                throw new ArgumentNullException("validators");
            if (validators.Length == 0)
                throw new ArgumentException(
                    ConsoleProgramMessages.OptionValidatorsNotSpecified.Fmt(CultureInfo.CurrentCulture, option.Name), "validators");
            foreach (BaseValidator validator in validators)
                option.Validators.Add(parameterIndex, validator);
        }

        internal static void HandledBy<T>(T option, OptionHandler handler) where T : Option
        {
            if (handler == null)
                throw new ArgumentNullException("handler");
            option.Handler = handler;
        }

        internal static void AddToList<T>(Option option, Expression<Func<IList<T>>> expression, Converter<string, T> converter = null,
            int parameterIndex = ParameterIndex.First)
        {
            if (option == null)
                throw new ArgumentNullException("option");
            if (expression == null)
                throw new ArgumentNullException("expression");

            option.Handler = prms => {
                converter = FluentHelpers.GetConverterFor(converter);

                MemberInfo dataMember = FluentHelpers.GetMemberInfoFromExpression(expression, typeof(IList<T>));
                var list = FluentHelpers.GetDataMemberValue<IList<T>>(dataMember);
                if (parameterIndex == ParameterIndex.All)
                {
                    foreach (string prm in prms)
                    {
                        object value = converter != null ? (object)converter(prm) : prm;
                        list.Add((T)value);
                    }
                } else
                {
                    object value = converter != null ? (object)converter(prms[parameterIndex]) : prms[parameterIndex];
                    list.Add((T)value);
                }
            };
        }

        internal static void AssignTo<T>(Option option, Expression<Func<T>> expression, Converter<string, T> converter = null,
            int parameterIndex = ParameterIndex.First)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");
            if (parameterIndex < 0)
                throw new ArgumentOutOfRangeException("parameterIndex", ConsoleProgramMessages.AssignToCannotSpecifyAllParameters);

            option.Handler = prms => {
                converter = FluentHelpers.GetConverterFor(converter);
                MemberInfo dataMember = FluentHelpers.GetMemberInfoFromExpression(expression);
                object value = converter != null ? (object)converter(prms[parameterIndex]) : prms[parameterIndex];
                FluentHelpers.SetDataMemberValue(dataMember, value);
            };
        }

        internal static void Flag(Option option, Expression<Func<bool>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");
            option.Handler = prms => {
                MemberInfo dataMember = FluentHelpers.GetMemberInfoFromExpression(expression);
                FluentHelpers.SetDataMemberValue(dataMember, true);
            };
        }
    }

    internal static class FluentHelpers
    {
        //Extracts the MemberInfo data from a member expression.
        //If baseType is specified, then the type specified in the expression must be assignable to
        //the baseType. Otherwise, it must be of type T.
        internal static MemberInfo GetMemberInfoFromExpression<T>(Expression<Func<T>> expression, Type baseType = null)
        {
            var lambda = (LambdaExpression)expression;
            var memberExpression = lambda.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException(
                    ConsoleProgramMessages.ExpressionNotDataMemberFunc.Fmt(CultureInfo.CurrentCulture, typeof(T).FullName), "expression");
            }
            if (baseType != null)
            {
                if (!baseType.IsAssignableFrom(memberExpression.Type))
                {
                    throw new ArgumentException(
                        ConsoleProgramMessages.ExpressionNotAssignableToBaseType.Fmt(CultureInfo.CurrentCulture,
                            memberExpression.Member.Name, baseType.FullName), "expression");
                }
            } else if (memberExpression.Type != typeof(T))
            {
                throw new ArgumentException(ConsoleProgramMessages.ExpressionNotEqualToType.Fmt(CultureInfo.CurrentCulture,
                    memberExpression.Member.Name, typeof(T)));
            }
            return memberExpression.Member;
        }

        internal static Type GetDataMemberType(MemberInfo member)
        {
            var property = member as PropertyInfo;
            if (property != null)
                return property.PropertyType;

            var field = member as FieldInfo;
            if (field != null)
                return field.FieldType;

            string declaringTypeName = member.DeclaringType != null ? member.DeclaringType.FullName : "Unknown";
            throw new ConsoleProgramException(ConsoleProgramException.Codes.TypeMemberNotData, ConsoleProgramMessages.TypeMemberNotData,
                member.Name, declaringTypeName);
        }

        //Given a data member, return its value
        internal static T GetDataMemberValue<T>(MemberInfo member)
        {
            var property = member as PropertyInfo;
            if (property != null)
                return (T)property.GetValue(null, null);

            var field = member as FieldInfo;
            if (field != null)
                return (T)field.GetValue(null);

            string declaringTypeName = member.DeclaringType != null ? member.DeclaringType.FullName : "Unknown";
            throw new ConsoleProgramException(ConsoleProgramException.Codes.TypeMemberNotData, ConsoleProgramMessages.TypeMemberNotData,
                member.Name, declaringTypeName);
        }

        //Sets the value of the given data member
        internal static void SetDataMemberValue(MemberInfo member, object value)
        {
            var property = member as PropertyInfo;
            if (property != null)
            {
                property.SetValue(null, value, null);
                return;
            }

            var field = member as FieldInfo;
            if (field != null)
            {
                field.SetValue(null, value);
                return;
            }

            string declaringTypeName = member.DeclaringType != null ? member.DeclaringType.FullName : "Unknown";
            throw new ConsoleProgramException(ConsoleProgramException.Codes.TypeMemberNotData, ConsoleProgramMessages.TypeMemberNotData,
                member.Name, declaringTypeName);
        }

        internal static Converter<string, T> GetConverterFor<T>(Converter<string, T> converter)
        {
            Type type = typeof(T);

            //If the type is string, return whatever converter was passed, even if it is null (which
            //would mean that the converter just uses the parameter value as-is
            if (type == typeof(string) || converter != null)
                return converter;

            //Special handling for enums
            if (type.IsEnum)
                return str => (T)Enum.Parse(type, str, true);

            //Special handling for booleans
            if (type == typeof(bool))
                return str => {
                    bool boolValue;
                    if (bool.TryParse(str, out boolValue))
                        return (T)(object)boolValue;

                    if (str.Equals("yes", StringComparison.OrdinalIgnoreCase) || str.Equals("1", StringComparison.OrdinalIgnoreCase))
                        return (T)(object)true;
                    if (str.Equals("no", StringComparison.OrdinalIgnoreCase) || str.Equals("0", StringComparison.OrdinalIgnoreCase))
                        return (T)(object)false;

                    throw new FormatException(string.Format("Invalid boolean value specified - {0}", str));
                };

            //For any basic type, return a pre-defined converter from the lookup dictionary
            Delegate @delegate;
            if (_converterLookup.TryGetValue(typeof(T), out @delegate))
                return (Converter<string, T>)@delegate;

            //If we cannot figure it out for the caller, then throw an exception saying that the caller
            //must explicitly specify the converter.
            throw new ConsoleProgramException(ConsoleProgramException.Codes.ConverterNotFound, ConsoleProgramMessages.ConverterNotFound,
                typeof(T).FullName);
        }

        //Lookup of converters for all basic types
        private static readonly Dictionary<Type, Delegate> _converterLookup = new Dictionary<Type, Delegate> {
            { typeof(int), (Converter<string, int>)int.Parse },
            { typeof(uint), (Converter<string, uint>)uint.Parse },
            { typeof(sbyte), (Converter<string, sbyte>)sbyte.Parse },
            { typeof(byte), (Converter<string, byte>)byte.Parse },
            { typeof(char), (Converter<string, char>)char.Parse },
            { typeof(short), (Converter<string, short>)short.Parse },
            { typeof(ushort), (Converter<string, ushort>)ushort.Parse },
            { typeof(long), (Converter<string, long>)long.Parse },
            { typeof(ulong), (Converter<string, ulong>)ulong.Parse },
            { typeof(float), (Converter<string, float>)float.Parse },
            { typeof(double), (Converter<string, double>)double.Parse },
            { typeof(decimal), (Converter<string, decimal>)decimal.Parse },
        };
    }
}